﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Upreader.Common.Cryptography;

namespace Upreader.Usenet.Nntp.Encodings
{
    public class YencEncodedStream : MetaLineStream, IEncodedStreamValidatable
    {
        #region Configuration

        // assuming the encoding is UTF-7
        private static readonly Encoding YencEncoding = NntpClient.DefaultTextEncoding;
        private static readonly string YencMetaLinePrefix = "=y";

        // basic decryption product
        private const byte Decryptor = 42;
        // when detected, indicatingthe next byte is escaped
        private const byte EscapedIndicator = 61;
        // decryption product for unescape
        private const byte UnescapeDecryptor = 64;
        
        private const byte LineFeed = 10;
        private const byte CarriageReturn = 13;

        #endregion // Configuration

        #region Fields

        private readonly Stream _baseStream;
        private readonly ICollection<YencMetaLine> _foundMetaLines;

        private readonly FromCRC32Transform _crc32Transform;

        // when set, the current byte will be an escaped byte and therefore should be unescaped
        private bool _isEscapedByte;


        #endregion // Fields

        #region Constructor

        public YencEncodedStream(Stream baseStream)
            : base(baseStream, YencMetaLinePrefix, YencEncoding)
        {
            if (baseStream == null)
                throw new ArgumentNullException("baseStream");

            _baseStream = baseStream;
            _foundMetaLines = new List<YencMetaLine>();
            _crc32Transform = new FromCRC32Transform();
        }

        #endregion // Constructor

        #region Properties

        public IEnumerable<YencMetaLine> FoundMetaLines
        {
            get { return _foundMetaLines; }
        }

        /// <summary>
        /// Get or set whether the current byte is an escape byte 
        /// </summary>
        public bool IsEscapedByte
        {
            get { return _isEscapedByte; }
            set { _isEscapedByte = value; }
        }

        #endregion // Properties

        #region Methods

        public EncodedStreamValidatonResult Validate()
        {
            _crc32Transform.TransformFinalBlock(new byte[0], 0, 0);
            byte[] calculatedHash = _crc32Transform.Hash;

            YencMetaLine endMetaLine = _foundMetaLines.FirstOrDefault(x => x.Name == "end");
            if (endMetaLine != null)
            {
                string checksumAttributeName;
                if (_foundMetaLines.Any(x => x.Name == "part"))
                {
                    checksumAttributeName = "pcrc32";
                }
                else
                {
                    checksumAttributeName = "crc32";
                }

                string attributeValue = endMetaLine.Attributes.Where(x => x.Key == checksumAttributeName)
                                                              .Select(x => x.Value)
                                                              .FirstOrDefault();

                if (attributeValue != null)
                {
                    int crc32 = int.Parse(attributeValue, System.Globalization.NumberStyles.HexNumber);
                    int calculatedCrc32 = BitConverter.ToInt32(calculatedHash.Reverse().ToArray(), 0);

                    if (crc32 == calculatedCrc32)
                    {
                        return EncodedStreamValidatonResult.Valid;
                    }
                    else
                    {
                        return EncodedStreamValidatonResult.Invalid;
                    }
                }
            }
            return EncodedStreamValidatonResult.Unknown;
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if (offset < 0)
                throw new ArgumentOutOfRangeException("offset");
            if (count < 0)
                throw new ArgumentOutOfRangeException("count");
            if (count > buffer.Length - offset)
                throw new ArgumentOutOfRangeException("count", "to many bytes were requested for the current buffer when starting from offset");

            int total = 0;

            while (total != count)
            {
                int currentOffset = offset + total;
                int currentLength = count - total;

                // read the next part in the buffer
                int currentByteCount = base.Read(buffer, currentOffset, currentLength);

                if (currentByteCount == 0) // this is the end of the stream, so break out of the loop
                    break;

                int writeBufferIndex = currentOffset;

                int maxOffset = currentOffset + currentByteCount;

                for (int readBufferIndex = currentOffset; readBufferIndex < maxOffset; readBufferIndex++)
                {
                    byte currentByte = buffer[readBufferIndex];

                    // when the current byte is an escaped byte, it cant have any special meaning
                    if (!_isEscapedByte)
                    {
                        if (currentByte == EscapedIndicator)
                        {
                            // set isEscapedByte for the next byte on true and continue with the next byte
                            _isEscapedByte = true;
                            continue;
                        }
                    }

                    // if we come here, the current byte is not an escapedIndicatorByte
                    // for each byte perform the decode logic
                    // this does not count for a raw lineFeed or carriage return which are part of the NNTP protocol and can be ignored
                    if (currentByte != LineFeed && currentByte != CarriageReturn)
                    {
                        if (_isEscapedByte)
                        {
                            // this is an escaped byte so unescape first
                            unchecked { currentByte -= UnescapeDecryptor; }

                            _isEscapedByte = false;
                        }

                        unchecked { currentByte -= Decryptor; }

                        // write away the current buffer in the current writeBufferIndex and increase that
                        buffer[writeBufferIndex] = currentByte;
                        writeBufferIndex++;
                    }
                }

                // and update total by simply checking how much the current buffer readBufferIndex has been moved upwards
                total += writeBufferIndex - currentOffset;
            }

            // transform the next part of the buffer
            _crc32Transform.TransformBlock(buffer, offset, total, null, 0);
            
            return total;
        }

        /// <summary>
        /// Not supported
        /// </summary>
        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Not supported
        /// </summary>
        public override void SetLength(long value)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Not supported
        /// </summary>
        public override void Write(byte[] buffer, int offset, int count)
        {
            throw new NotSupportedException();
        }

        protected override void OnMetaLineFound(string line)
        {
            YencMetaLine metaLine = new YencMetaLine(line);
            _foundMetaLines.Add(metaLine);
        }

        #endregion // Methods

        #region Disposable

        #region Helpers

        #endregion // Helpers

        protected override void Dispose(bool disposing)
        {
            try
            {
                if (disposing)
                {
                    BaseStream.Dispose();
                }
            }
            finally
            {
                base.Dispose(disposing);
            }

        }

        #endregion // Disposable
    }
}
